﻿using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;  
  
namespace WaterExample
{
    ///<summary>
    ///Abstract camera class.
    ///Is a game component that implements IUpdateable.
    ///</summary>
    public abstract class Camera : GameComponent
    {
        #region variables
        public BoundingFrustum BoundingFrustum { get; private set; }

        private bool _isAppActive = true;
        public bool IsAppActive
        {
            get { return _isAppActive; }
            set { _isAppActive = value; }
        }

        public static Matrix View;  
        public static Matrix Projection;
        public static Vector3 Position;
        #endregion


        public static void SetPosition(Vector3 pos)
        {
            Position = pos;
        }

        /// <summary>
        /// Initialize a new instance of this class.
        /// Abstract camera class.
        /// Is basic for each other camera class
        /// </summary>
        protected Camera(Game game)
            : base(game) { }

        /// <summary>
        /// Generate the projection matrix
        /// </summary>
        public override void Initialize()
        {
            GeneratePerspectiveProjectionMatrix(MathHelper.PiOver2);
            base.Initialize();
        }

        //Create Projection Matrix
        private void GeneratePerspectiveProjectionMatrix(float fov)
        {
            PresentationParameters pp = Game.GraphicsDevice.PresentationParameters;
            float aspectRatio = (float)pp.BackBufferWidth / pp.BackBufferHeight;
            Projection = Matrix.CreatePerspectiveFieldOfView(fov, aspectRatio, 0.1f, 10000f);
        }

        #region Frustum Culling

        /// <summary>
        /// Generate a frustum to cull unnecessary objects
        /// </summary>
        private void GenerateFrustum()
        {
            Matrix viewProjection = View * Projection;
            BoundingFrustum = new BoundingFrustum(viewProjection);
        }

        /// <summary>
        /// Checks if bounding sphere is in view frustum of camera
        /// </summary>
        /// <param name="boundingSphere">Boundingsphere to check, if it is in view of camera</param>
        /// <returns>True if boundingsphere is in view of camera</returns>
        public bool IsBoundingVolumeInView(BoundingSphere boundingSphere)
        {
            return BoundingFrustum.Contains(boundingSphere) != ContainmentType.Disjoint;
        }

        /// <summary>
        /// Checks if bounding box is in view frustum of camera
        /// </summary>
        /// <param name="boundingBox">Boundingbox to check, if it is in view of camera</param>
        /// <returns>True if boundingbox is in view of camera</returns>
        public bool IsBoundingVolumeInView(BoundingBox boundingBox)
        {
            return BoundingFrustum.Contains(boundingBox) != ContainmentType.Disjoint;
        }
        #endregion

        /// <summary>
        /// Force Update method for subclasses
        /// </summary>
        /// <param name="gameTime">Actual time of the game</param>
        public override void Update(GameTime gameTime)
        {
        }
    }

    /// <summary>
    /// Subclass of abstract camera class.
    /// Looks at a specific point.
    /// </summary>
    public class TargetCamera : Camera
    {
        #region variables

        private Vector3 _target;
        public Vector3 Target
        {
            get { return _target; }
        }

        private Vector3 _lookAt;
        public Vector3 LookAt
        {
            get { return _lookAt; }
        }

        private Vector3 _side;
        public Vector3 Side
        {
            get { return _side; }
        }
        private Vector3 _forward;
        public Vector3 Forward
        {
            get { return _forward; }
        }

        public Vector3 Up
        {
            get { return Vector3.Cross(_lookAt, _side); }
        }

        private MouseState _lastMouseState;
        private Vector2 _mouseDistance;

        #endregion

        /// <summary>
        /// Initialize a new instance of this class.
        /// This Camera looks at a given target.
        /// </summary>
        /// <param name="position">Position of the camera.</param>
        /// <param name="target">Position of the target, where the camera should look at</param>
        /// <param name="game">Game</param>
        public TargetCamera(Vector3 position, Vector3 target, Game game)
            : base(game)
        {
            SetPosition(position);
            _target = target;
            _lookAt = target - position;
        }


        /// <summary>
        /// Rotates the camera
        /// </summary>
        /// <param name="yawChange">Yaw change value of camera</param>
        /// <param name="pitchChange">Pitch change value of camera</param>
        public void Rotate(float yawChange, float pitchChange)
        {
            pitchChange = MathHelper.Clamp(pitchChange, -0.2f, 0.2f);

            _lookAt = Vector3.TransformNormal(_lookAt, Matrix.CreateFromAxisAngle(_side, pitchChange));
            _lookAt = Vector3.TransformNormal(_lookAt, Matrix.CreateFromAxisAngle(Vector3.Up, yawChange));

            _lookAt.Normalize();
            if (Math.Abs(Vector3.Dot(Vector3.Up, _lookAt)) > 0.99)
                _lookAt = Vector3.TransformNormal(_lookAt, Matrix.CreateFromAxisAngle(_side, -pitchChange));
        }

        /// <summary>
        /// Translate the camera  of given Vector
        /// </summary>
        /// <param name="positionChange">Position change value</param>
        public void Translate(Vector3 positionChange)
        {
            Position += positionChange.Z * _forward;
            Position += positionChange.X * _side;
            Position += positionChange.Y * Vector3.Up;
        }

        /// <summary>
        /// Update the camera and handles input data of mouse and keyboard.
        /// </summary>
        public override void Update(GameTime gameTime)
        {
            if (IsAppActive)
            {
                MouseState currentMouseState = Mouse.GetState();

                _mouseDistance.X = currentMouseState.X - _lastMouseState.X;
                _mouseDistance.Y = currentMouseState.Y - _lastMouseState.Y;

                //Slow mouse down
                _mouseDistance *= 0.1f;

                //Initialize variables
                Vector3 translation = Vector3.Zero;
                float yaw = 0;
                float pitch = 0;

                #region Mouse Controls

                //Camera Y=Forward/Backward + X=Left/Right Rotation
                if (currentMouseState.LeftButton == ButtonState.Pressed &&
                    currentMouseState.RightButton != ButtonState.Pressed)
                {
                    translation.Z -= _mouseDistance.Y;
                    yaw = -_mouseDistance.X;
                }
                //Camera Rotate
                if (currentMouseState.LeftButton != ButtonState.Pressed &&
                    currentMouseState.RightButton == ButtonState.Pressed)
                {
                    pitch = -_mouseDistance.Y;
                    yaw = -_mouseDistance.X;
                }
                //Camera Up/Down + Left/Right
                if (currentMouseState.LeftButton == ButtonState.Pressed &&
                    currentMouseState.RightButton == ButtonState.Pressed)
                {
                    translation.Y -= _mouseDistance.Y;
                    translation.X += _mouseDistance.X;
                }

                #endregion

                #region Keyboard Controls

                if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.W))
                {
                    translation += Vector3.Backward;
                }
                if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.S))
                {
                    translation += Vector3.Forward;
                }
                if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.A))
                {
                    translation += Vector3.Left;
                }
                if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.D))
                {
                    translation += Vector3.Right;
                }

                #endregion

                //Relate translation/yaw/pitch to gametime, so not relies on systemspeed
                translation *= (float)gameTime.ElapsedGameTime.TotalMilliseconds * 0.05f;
                yaw *= (float)gameTime.ElapsedGameTime.TotalMilliseconds * 0.001f;
                pitch *= (float)gameTime.ElapsedGameTime.TotalMilliseconds * 0.001f;

                //Reset mouse to center, so that the mouse is not blocked by the monitor size
                Mouse.SetPosition(Game.GraphicsDevice.Viewport.Width / 2, Game.GraphicsDevice.Viewport.Height / 2);


                //Submit Manipulationdata to TargetCamera Class
                Rotate(yaw, pitch);
                Translate(translation);

                //Update MouseStates
                currentMouseState = Mouse.GetState();
                _lastMouseState = currentMouseState;


                _side = Vector3.Normalize(Vector3.Cross(_lookAt, Vector3.Up));
                _forward = Vector3.Normalize(Vector3.Cross(Vector3.Up, _side));

                _target = Position + _lookAt;
                View = Matrix.CreateLookAt(Position, _target, Vector3.Up);

                base.Update(gameTime);
            }
        }
    }
}
